查看原文
其他

对VM逆向的分析 | 一个经典的虚拟机逆向CTF题

SYJ-Re 看雪学苑 2022-07-01
本文为看雪论坛精华文章
看雪论坛作者ID:SYJ-Re


简单的vm-re框架
 
虚拟机就是要去模仿一个机器,让机器去执行一个文件(类似用win10去执行一个文件)
 
首先它需要一些在CPU中的寄存器和内存中的堆栈,这样去模拟一个CPU不断的去读取指令。主要是以循环的形式进行读取。
 
1. 在全局变量中分配如下内容:
这里就构成了CPU+内存。
 
2. 模拟一个CPU读取指令的形式(dispatcher)去写这样一个主程序。
 
主程序:其实就是一个循环,这个循环不断的去读取指令(伪机器码opcode)这个会存在于内存中或文件中,然后执行指令opcode所对应的一些函数(其实这些函数可以理解为伪汇编的执行过程,比如就像一些add,sub,pop,push,jmp等操作),这样下来就可以与真实的程序执行相差无几。


一  WriteUp


拿到程序之后先查壳,发现无壳之后运行一下查看,只有一个plz input:
 
我们将其直接拖入ida中进行查看,左侧函数窗口CTRL+F查找main函数。
 
跳转到main函数之后发现有个花指令:
%2007d4e31ff53d45308e7309126226c678/L0__6X1NGME7LX43BB.png)

将地址0x401594处的0xB8换成0x90(nop的硬编码)即可,然后P一下main函数按Tab进行反汇编。
 
如下:
int __cdecl main(int argc, const char **argv, const char **envp){ unsigned int i; // [esp+10h] [ebp-8h] sub_6914F0(); while ( 1 ) {LABEL_2: for ( i = 0; ; i += 2 ) { if ( i >= 0x58 ) goto LABEL_2; if ( dword_6D48F0[4 * i] == opcode_team[ei_p] )// opcode_team存在于内存中的伪机器码(指令) break; } dispatcher[i](); // 模拟一个CPU不断的去执行指令(其实就是根据opcode去执行一些模拟汇编的一些函数) }}

dispatcher就是去实现模拟CPU读取指令。opcode_team就是存在于内存中(也可存在于文件中)的opcode 伪机器码。
 
点开dispatcher,依次进行分析如下(这里就是一些模拟汇编的伪汇编函数)
 
分析之后的:

分析过程:(不一一举例,举几个例子)
什么也没做,就只是++了eip,说明该函数模拟的是NOP。

这里点击过去发现了连续的一个数组和一些字符串数据,说明模拟的是内存和CPU中的寄存器。
这里模拟的就是mov reg, data,又比如sub_691070。

这个模拟的就是push data,同时我们也在内存中找到了模拟栈。

 
以此类推,将全部伪汇编的函数分析出来,然后写出解析脚本:
opcode_team = [0x01, 0x03, 0x03, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x01, 0x01, 0x11, 0x0C, 0x00, 0x01, 0x0D, 0x0A, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x00, 0x11, 0x0C, 0x00, 0x02, 0x0D, 0x2B, 0x00, 0x14, 0x00, 0x02, 0x01, 0x01, 0x61, 0x0C, 0x00, 0x01, 0x10, 0x1A, 0x00, 0x01, 0x01, 0x7A, 0x0C, 0x00, 0x01, 0x0F, 0x1A, 0x00, 0x01, 0x01, 0x47, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 0x00, 0x01, 0x0B, 0x24, 0x00, 0x01, 0x01, 0x41, 0x0C, 0x00, 0x01, 0x10, 0x24, 0x00, 0x01, 0x01, 0x5A, 0x0C, 0x00, 0x01, 0x0F, 0x24, 0x00, 0x01, 0x01, 0x4B, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x07, 0x00, 0x01, 0x01, 0x01, 0x10, 0x09, 0x00, 0x01, 0x03, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x0B, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x02, 0x01, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x00, 0x00, 0x02, 0x09, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x03, 0x00, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x05, 0x00, 0x02, 0x03, 0x00, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00, 0x02, 0x07, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0B, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x02, 0x02, 0x00, 0x02, 0x07, 0x00, 0x02, 0x02, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x02, 0x00, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x13, 0x01, 0x02, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x01, 0x0E, 0x5B, 0x00, 0x01, 0x01, 0x22, 0x0C, 0x02, 0x01, 0x0D, 0x59, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x4E, 0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]opcode_key = { 0: 'nop', 1: 'mov reg data', 2: 'push data', 3: 'push_reg', 4: 'pop_reg', 5: 'printf', 6: 'add_reg_reg1', 7: 'sub_reg_reg1', 8: 'mul', 9: 'div', 10: 'xor', 11: 'jmp', 12: 'cmp', 13: 'je', 14: 'jne', 15: 'jg', 16: 'jl', 17: 'scanf_strlen', 18: 'mem_init', 19: 'stack_to_reg', 20: 'load_input', 0xff: 'exit'}count = 0code_index = 1for x in opcode_team: # 取每个opcode if count % 3 == 0: # 每3个opcode是一条指令,每个指令的第一个是操作码 print(str(code_index)+':', end='') print(opcode_key[x], end=' ') code_index += 1 elif count % 3 == 1: print(str(x)+',', end=' ') else: print(str(x)) count += 1

//打印出来的结果如下1:mov reg data 3, 32:printf 0, 03:scanf_strlen 0, 04:mov reg data 1, 175:cmp 0, 16:je 10, 07:mov reg data 3, 18:printf 0, 09:exit 0, 010:mov reg data 2, 011:mov reg data 0, 1712:cmp 0, 213:je 43, 014:load_input 0, 215:mov reg data 1, 9716:cmp 0, 117:jl 26, 018:mov reg data 1, 12219:cmp 0, 120:jg 26, 021:mov reg data 1, 7122:xor 0, 123:mov reg data 1, 124:add_reg_reg1 0, 125:jmp 36, 026:mov reg data 1, 6527:cmp 0, 128:jl 36, 029:mov reg data 1, 9030:cmp 0, 131:jg 36, 032:mov reg data 1, 7533:xor 0, 134:mov reg data 1, 135:sub_reg_reg1 0, 136:mov reg data 1, 1637:div 0, 138:push_reg 1, 039:push_reg 0, 040:mov reg data 1, 141:add_reg_reg1 2, 142:jmp 11, 043:push data 7, 044:push data 13, 045:push data 0, 046:push data 5, 047:push data 1, 048:push data 12, 049:push data 1, 050:push data 0, 051:push data 0, 052:push data 13, 053:push data 5, 054:push data 15, 055:push data 0, 056:push data 9, 057:push data 5, 058:push data 15, 059:push data 3, 060:push data 0, 061:push data 2, 062:push data 5, 063:push data 3, 064:push data 3, 065:push data 1, 066:push data 7, 067:push data 7, 068:push data 11, 069:push data 2, 070:push data 1, 071:push data 2, 072:push data 7, 073:push data 2, 074:push data 12, 075:push data 2, 076:push data 2, 077:mov reg data 2, 178:stack_to_reg 1, 279:pop_reg 0, 080:cmp 0, 181:jne 91, 082:mov reg data 1, 3483:cmp 2, 184:je 89, 085:mov reg data 1, 186:add_reg_reg1 2, 187:jmp 78, 088:mov reg data 3, 089:printf 0, 090:exit 0, 091:mov reg data 3, 192:printf 0, 093:exit 0, 094:nop

分析和注释:

mov edx, 3printf(...)scanf(our_input)mov eax, strlen(our_input)mov ebx, 17cmp eax, ebxje (10) //相等才跳转到position0mov edx, 1printf(...)exit(0) //不相等就退出了(10)mov ecx, 0mov eax, 17cmp eax, ecxje (43) //直至计数器等于17eax = load_input[ecx] //循环读取我们的inputmov ebx, 'a'cmp eax, ebxjl (26) //该字符小于'a'则跳转到26mov ebx, 'z'cmp eax, ebxjg (26) //该字符大于'z'则跳转到26mov ebx, 71xor eax, ebx mov ebx, 1add eax, ebx //否则将该字符(char^71+1)jmp (36)(26)mov ebx, 'A'cmp eax, ebxjl (36) //该字符比'A'小则跳转到36mov ebx, 'Z'cmp eax, ebxjg (36) //该字符比'Z'大则跳转到36mov ebx, 75 xor eax, ebxmov ebx, 1sub eax, ebx //否则将该字符(char^75-1) (36)mov ebx, 16div eax, ebx //将该字符%16push ebx push eax //先压入整除的数字,再压入余数mov ebx, 1mov ecx, 1jmp(11)(43)push //压入一连串的数据,......(76)push 2(77)mov ecx, 1(78)stack_to_reg 1, 2pop eaxcmp eax, ebxjne (91)mov ebx, 34 //17拆成34个(整数和余数)cmp ecx, ebxje (89)mov ebx, 1mov ecx, 1jmp(78)mov edx, 0 //设置返回值为0(89)printf(..) //打印失败字符串exit(..) //退出程序(91)mov edx, 1 //设置返回值为1printf(..) //打印成功字符串# exit(..) //退出程序nop

最后理解程序的逻辑之后写出解题脚本:

data = [0x7,0xd,0x0,0x5,0x1,0xc,0x1,0x0,0x0,0xd,0x5,0xf,0x0,0x9,0x5,0xf,0x3,0x0,0x2,0x5,0x3,0x3,0x1,0x7,0x7,0xb,0x2,0x1,0x2,0x7,0x2,0xc,0x2,0x2,]data = data[::-1]flag = ''for i in range(0, 34, 2): temp = data[i] + data[i+1]*16 x = ((temp+1) ^ 75) y = ((temp-1) ^ 71) if x>=65 and x<=90: # 'A'-'Z' flag += chr(x) elif y>=97 and y<=122: # 'a' - 'z' flag += chr(y) else: flag += chr(temp) # 没有处于'a'-'z'或'A'-'Z'之间print(flag)


得到flag值为:
flag{Such_A_EZVM}


二  总结


再遇见vm-re的题目,先查找到opcode(伪机器码),然后找到dispatcher(就是模拟CPU读取指令的分发器),然后边分析那些伪汇编函数(就是模仿汇编指令的函数)边查找模拟的CPU的栈,寄存器,全局变量(多是字符串)等。
 
参考资料:
  • https://www.bilibili.com/video/BV1Nv411k7HJ

  • https://blog.csdn.net/weixin_43876357/article/details/108488762‍





 


看雪ID:SYJ-Re

https://bbs.pediy.com/user-home-921830.htm

  *本文由看雪论坛 SYJ-Re 原创,转载请注明来自看雪社区。



《安卓高级研修班》
2021年6月班火热招生中!



# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存